feat(server): add evaluateFlags() API for single-call flag evaluation#498
Open
feat(server): add evaluateFlags() API for single-call flag evaluation#498
Conversation
Adds a `PostHogFeatureFlagEvaluations` snapshot returned by `PostHogInterface.evaluateFlags(distinctId)`. The snapshot exposes `isEnabled`/`getFlag`/`getFlagPayload` plus `onlyAccessed()` and `only(keys)` filters; accessor calls fire deduped `$feature_flag_called` events with `$feature_flag_id`/`$feature_flag_version`/`$feature_flag_reason` metadata, an empty-distinctId snapshot short-circuits all events, and filtered clones keep their own access set. `capture()` gains a `flags` parameter that takes a snapshot and attaches `$feature/<key>` and `$active_feature_flags` without making a second `/flags` request. The dedup helper on `PostHogStateless` is split into `captureFeatureFlagCalledEvent` so both the per-flag accessor and the snapshot share the same per-distinct-id LRU. Locally-evaluated flags carry a "local_evaluation" reason and the snapshot stamps `locally_evaluated=true` plus `$feature_flag_definitions_loaded_at` to match posthog-node and posthog-python. Adds `featureFlagsLogWarnings` config option to silence filter-helper warnings, threads `flagKeys` and `geoip_disable` into the `/flags` request body, and ships JUnit coverage for the snapshot, dedup, empty-distinctId, local-evaluation, and `capture(flags=)` paths. The existing `appendFeatureFlags = true` capture path is preserved unchanged; deprecation of the per-flag accessors is Phase 2. Generated-By: PostHog Code Task-Id: 87de4c67-f607-4432-b8ee-3c059e368f81
91d0f41 to
f60ec17
Compare
Contributor
posthog-android Compliance ReportDate: 2026-04-29 21:16:23 UTC
|
| Test | Status | Duration |
|---|---|---|
| Request Payload.Request With Person Properties Device Id | ❌ | 238ms |
Failures
request_payload.request_with_person_properties_device_id
404, message='Not Found', url='http://sdk-adapter:8080/get_feature_flag'
Phase 1 (already on this branch): `evaluateFlags(distinctId)` snapshot,
`capture(flags = …)`, dedup helper extraction, full metadata on
`$feature_flag_called`.
Phase 2 (new in this commit):
- `@Deprecated` annotations on `isFeatureEnabled`, `getFeatureFlag`,
`getFeatureFlagPayload`, and `getFeatureFlagResult` (all overloads on
both `PostHogInterface` and the `PostHog` server class), with messages
pointing callers at `evaluateFlags(...)`.
- Runtime deprecation log when `capture(appendFeatureFlags = true)` is
used — mirrors the Python PR's "only-when-truthy" runtime warning,
since Kotlin can't deprecate a single parameter value at compile time.
- All legacy paths keep working unchanged; deprecations can be silenced
with `@Suppress("DEPRECATION")`. Removal is targeted at the next major.
Also: response-level errors (`errors_while_computing_flags`,
`quota_limited`) are now propagated into snapshot
`$feature_flag_called` events as `$feature_flag_error`, matching the
granularity the per-flag accessor path emits.
New tests:
- `responseError` propagates to `$feature_flag_called` (unit + integration).
- `appendFeatureFlags = true` deprecated path still attaches feature
properties end-to-end.
Generated-By: PostHog Code
Task-Id: 87de4c67-f607-4432-b8ee-3c059e368f81
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
Phase 1 + Phase 2 of the Server SDK Feature Flag Evaluations RFC for the JVM
posthog-serverpackage. Companion to the Node SDK PR (PostHog/posthog-js#3476) and the Python SDK PR (PostHog/posthog-python#539).Today every flag check on
posthog-serverfires its own/flagsrequest, andcapture(appendFeatureFlags = true)silently fires yet another on every captured event. The flag values on a captured event can diverge from the ones the code actually branched on when person/group properties differ between calls.appendFeatureFlagsalso attaches every evaluated flag to every event, which bloats properties on high-volume events.Tracking RFC: requests-for-comments-internal#1020.
Changes
New API (Phase 1)
postHog.evaluateFlags(distinctId, …)returns aPostHogFeatureFlagEvaluationssnapshot:A single
/flagsrequest powers both branching and event enrichment.isEnabled()andgetFlag()fire$feature_flag_calledevents (deduped through the existing per-distinct-id LRU) with the full metadata —$feature_flag_id,$feature_flag_version,$feature_flag_reason,$feature_flag_request_id,$feature_flag_error, plus$feature_flag_definitions_loaded_atfor locally-evaluated flags — so experiment exposure tracking keeps working.Java callers get a
PostHogEvaluateFlagsOptions.builder()analogue alongside the Kotlin named-args entry point:Two layers of scoping
Network-level (
flagKeysoption): scopes the underlying/flagsrequest itself. Goes into the request body asflag_keys_to_evaluate.Event-level (filter helpers): narrow which flags get attached to a captured event without re-fetching.
Both
onlyAccessed()andonly(...)clone the snapshot with their ownaccessedset so filtered views don't back-propagate into the parent.Deprecations (Phase 2)
The legacy single-flag surface keeps working but is now
@Deprecated:isFeatureEnabled(...)getFeatureFlag(...)getFeatureFlagPayload(...)getFeatureFlagResult(...)capture(appendFeatureFlags = true)— emits a one-line deprecation log at runtime when truthy (Kotlin can't deprecate a single parameter value at compile time)Each
@Deprecatedannotation includes a message pointing atevaluateFlags(...). Phase 3 (removal in next major) ships separately.New config option
PostHogConfig.featureFlagsLogWarnings(defaulttrue) — set tofalseto silence the warnings emitted by theonlyAccessed()(empty-access fallback) andonly(...)(unknown-key drop) filter helpers. Useful for callers who use those helpers conditionally.Other request-body additions
evaluateFlags(...)also takesdisableGeoip = trueto forwardgeoip_disableinto the/flagsrequest body, parallel to the option that already exists in posthog-node and posthog-python.Local evaluation
Transparent. When the poller resolves a flag, the snapshot carries
locally_evaluated = true, reason"Evaluated locally", and$feature_flag_definitions_loaded_atis plumbed through, matching what the per-flag local path emits today.Backwards compatibility
No breaking changes. All existing call paths return the same values they did before. Kotlin callers see a
@Deprecatedcompile-time warning (silenceable with@Suppress("DEPRECATION")); theappendFeatureFlags = trueruntime log goes through the standard config logger.Internals
PostHogStateless.sendFeatureFlagCalledis split intocaptureFeatureFlagCalledEvent, which is shared between the per-flag accessor path and the new snapshot. Both paths now dedupe identically against the samePostHogFeatureFlagCalledCacheLRU. The single-flag path also picks up$feature_flag_id/$feature_flag_version/$feature_flag_reasonhere — previously only the Android client emitted those, server SDK was missing them.A small
EvaluationsHostinterface is what the snapshot calls back into, instead of holding a reference to the full client — keeps the snapshot easy to test in isolation with a fake host.Response-level errors (
errors_while_computing_flags,quota_limited) are propagated into snapshot$feature_flag_calledevents viaEvaluateFlagsResult.responseError, matching the granularity of the per-flag path.A new
getFeatureFlagDetails(...)default method onPostHogFeatureFlagsInterface(returnsnullby default) lets the server SDK expose the cachedFeatureFlagto the per-flag path without changing existing implementations.Docs:
posthog-server/USAGE.mdupdated with a Kotlin + Java example of the snapshot flow and theflagKeysvsonly(...)distinction.Tests
Two test files cover snapshot semantics and end-to-end flow:
PostHogFeatureFlagEvaluationsTest— snapshot accessors, full metadata on captures, filter helpers,onlyAccessed()empty-fallback warning,only(...)unknown-key warning, parent/child filter isolation, blank-distinctId no-op, locally-evaluated tagging, response-error propagation,featureFlagsLogWarnings = falsehost suppression.PostHogEvaluateFlagsTest— end-to-end viaMockWebServer: single/flagsround-trip, no events fire before access, dedup across access,getFlagPayloadno-event,capture(flags=…)doesn't issue a second request,flag_keys_to_evaluatebody forwarding, blank-distinctId short-circuit, local evaluation tagging,quota_limitedpropagation,appendFeatureFlags = truedeprecated path still works end-to-end../gradlew :posthog-server:check :posthog:check(tests + lint + apiCheck + animalsniffer) clean.Created with PostHog Code